/*
 * TcpClient.as
 *
 * Copyright (C) 2011 - Dr.NP
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * BS.Play TCP client, ActionScript port
 * Communicate with BSP server via adobe flash / AIR applications
 * 
 * @package org.bsgroup.bsp.client
 * @author Dr.NP <np@bsgroup.org>
 * @update 08/14/2012
 * @changelog 
 * 		[07/10/2012] - Creation
 * 		[08/14/2012] - Public method rebuild
 */

package org.bsgroup.bsp.client
{
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	import flash.events.DataEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.SecurityErrorEvent;
	import flash.events.TimerEvent;
	import flash.net.Socket;
	import flash.sensors.Accelerometer;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	import flash.utils.Timer;
	
	import org.bsgroup.bsp.inc.BSPConstants;
	import org.bsgroup.bsp.events.BSPEvent;
	import org.bsgroup.bsp.protocol.BSPPacket;
	
	public class TCPClient extends EventDispatcher
	{
		public var connected								: Boolean = false;
		public var compress_type							: uint;
		public var length_mark								: uint;
		private var sck										: Socket;
		private var tcp_addr								: String;
		private var tcp_port								: uint;
		private var client_type								: uint;
		private var data_type								: uint;
		private var packet									: BSPPacket;
		private var streams									: Array;
		private var packets									: Array;
		private var prev_hdr								: int;
		private var prev_size								: uint;
		private var hb_timer								: Timer = null;
		private var hb_wait									: uint = 0;
		private var hb_failure								: int;
		private var last_hb									: int;
		
		// Portal
		public function TCPClient(
								addr : String, 
								port : uint, 
								type : uint = BSPConstants.DATA_TYPE_STREAM, 
								heartbeat : int = -1, 
								heartbeat_failure : int = 10)
		{
			this.sck = new Socket();
			this.sck.endian = Endian.BIG_ENDIAN;
			
			this.sck.addEventListener(Event.CONNECT, this._on_connect);
			this.sck.addEventListener(Event.CLOSE, this._on_close);
			this.sck.addEventListener(IOErrorEvent.IO_ERROR, this._on_io_error);
			this.sck.addEventListener(SecurityErrorEvent.SECURITY_ERROR, this._on_sec_error);
			this.sck.addEventListener(ProgressEvent.SOCKET_DATA, this._on_data);
			
			this.tcp_addr = addr;
			this.tcp_port = port;
			this.packet = new BSPPacket();
			// AS3 client's type, RAW regularly
			this.client_type = BSPConstants.CLIENT_TYPE_RAW;
			this.data_type = (type == BSPConstants.DATA_TYPE_PACKET) ? BSPConstants.DATA_TYPE_PACKET : BSPConstants.DATA_TYPE_STREAM;
			this.compress_type = BSPConstants.COMPRESS_TYPE_NONE;
			this.length_mark = BSPConstants.PACKET_MARK_32B;
			this.streams = new Array();
			this.packets = new Array();
			this.prev_hdr = -1;
			this.prev_size = 0xFFFFFFFF;
			
			if (heartbeat > 0 && this.data_type == BSPConstants.DATA_TYPE_PACKET)
			{
				this.hb_timer = new Timer(heartbeat * 1000);
				this.hb_timer.addEventListener(TimerEvent.TIMER, this._hb);
				this.hb_failure = heartbeat_failure;
			}
			
			this.last_hb = -1;
			
			return;
		}
		
		// Set address and port
		public function set addr(addr : String) :  void
		{
			this.tcp_addr = addr;
			
			return;
		}
		
		public function set port(port : uint) : void
		{
			this.tcp_port = port;
			
			return;
		}
		
		// Connect to server
		public function connect() : void
		{
			if (this.tcp_addr && this.tcp_port)
			{
				this.sck.connect(this.tcp_addr, this.tcp_port);
			}
			
			return;
		}
		
		// Disconnect from server
		public function disconnect() : void
		{
			if (this.sck.connected)
			{
				this.sck.close();
				// Trigger event manually
				dispatchEvent(new BSPEvent(BSPEvent.BSP_CLOSE));
			}
			
			if (this.hb_timer)
			{
				this.hb_timer.stop();
			}
			
			return;
		}
		
		private function _generate_hdr(packet_type : uint, compress_type : uint, length_mark : uint) : int
		{
			var value : uint = ((packet_type & 7) << 5) | ((compress_type & 7) << 2) | (length_mark & 3);
			
			return value;
		}
		
		private function _real_send(data : ByteArray, type : int = -1) : uint
		{
			if (this.sck.connected)
			{
				if (type >= 0 && this.data_type == BSPConstants.DATA_TYPE_PACKET)
				{
					// Packet
					sck.writeByte(this._generate_hdr(type, this.compress_type, this.length_mark));
					
					if (data)
					{
						if (BSPConstants.COMPRESS_TYPE_DEFLATE == this.compress_type)
						{
							data.deflate();
						}
						if (BSPConstants.PACKET_MARK_64B == this.length_mark)
						{
							sck.writeUnsignedInt(0);
						}
						sck.writeUnsignedInt(data.length);
					}
				}
				
				if (data)
				{
					this.sck.writeBytes(data);
				}
				this.sck.flush();
				
				return data ? data.length : 0;
			}
			
			return 0;
		}
		
		// Send data to socket
		public function send_stream(stream : ByteArray) : ByteArray
		{
			if (!stream)
			{
				return null;
			}
			
			return (0 < this._real_send(stream)) ? stream : null;
		}
		
		public function send_raw(raw : ByteArray) : ByteArray
		{
			if (BSPConstants.DATA_TYPE_PACKET != this.data_type || !raw)
			{
				return null;
			}
			
			return (0 < this._real_send(raw, BSPConstants.PACKET_TYPE_RAW)) ? raw : null;
		}
		
		public function send_obj(obj : Object) : ByteArray
		{
			if (BSPConstants.DATA_TYPE_PACKET != this.data_type || !obj)
			{
				return null;
			}
			
			var s : ByteArray = this.packet.pack(obj, this.length_mark);
			return (0 < this._real_send(s, BSPConstants.PACKET_TYPE_OBJ)) ? s : null;
		}
		
		public function send_cmd(cmd : int, params : Object) : ByteArray
		{
			if (BSPConstants.DATA_TYPE_PACKET != this.data_type || !params)
			{
				return null;
			}
			
			var s : ByteArray = this.packet.pack(params, this.length_mark);
			var r : ByteArray = new ByteArray();
			r.endian = Endian.BIG_ENDIAN;
			r.writeInt(cmd);
			r.writeBytes(s);
			
			return (0 < this._real_send(r, BSPConstants.PACKET_TYPE_CMD)) ? r : null;
		}
		
		public function send_rep(compress : uint = BSPConstants.COMPRESS_TYPE_NONE, mark : uint = BSPConstants.PACKET_MARK_32B) : void
		{
			if (BSPConstants.DATA_TYPE_PACKET != this.data_type)
			{
				return;
			}
			
			var r : ByteArray = new ByteArray();
			r.endian = Endian.BIG_ENDIAN;
			r.writeByte(this._generate_hdr(BSPConstants.PACKET_TYPE_REP, compress, mark));
			this._real_send(r);
			
			return;
		}
		
		public function send_hb() : void
		{
			if (BSPConstants.DATA_TYPE_PACKET != this.data_type || !this.hb_timer || !this.hb_timer.running)
			{
				return;
			}
			
			var r : ByteArray = new ByteArray();
			r.endian = Endian.BIG_ENDIAN;
			r.writeByte(this._generate_hdr(BSPConstants.PACKET_TYPE_HEARTBEAT, 0, 0));
			this._real_send(r);
			
			return;
		}
		
		public function get_packet() : Object
		{
			//return this.packet.unpack(this.sck);
			if (this.packets.length > 0)
			{
				return this.packets.shift();
			}
			
			return null;
		}
		
		public function get_stream() : ByteArray
		{
			if (this.streams.length > 0)
			{
				return this.streams.shift();
			}
			
			return null;
		}
		
		/* Events */
		private function _on_connect(e : Event) : void
		{
			dispatchEvent(new BSPEvent(BSPEvent.BSP_CONNECT));
			if (this.hb_timer)
			{
				this.hb_timer.start();
			}
			this.connected = true;
			return;
		}
		
		private function _on_close(e : Event) : void
		{
			dispatchEvent(new BSPEvent(BSPEvent.BSP_CLOSE));
			if (this.hb_timer)
			{
				this.hb_timer.stop();
			}
			this.connected = false;
			return;
		}
		
		private function _on_io_error(e : IOErrorEvent) : void
		{
			dispatchEvent(new BSPEvent(BSPEvent.BSP_IO_ERROR));
			if (this.hb_timer)
			{
				this.hb_timer.stop();
			}
			this.connected = false;
			return;
		}
		
		private function _on_sec_error(e : SecurityErrorEvent) : void
		{
			dispatchEvent(new BSPEvent(BSPEvent.BSP_SEC_ERROR));
			if (this.hb_timer)
			{
				this.hb_timer.stop();
			}
			this.connected = false;
			return;
		}
		
		private function _on_data(e : ProgressEvent) : void
		{
			if (this.data_type == BSPConstants.DATA_TYPE_PACKET)
			{
				// Get header
				var hdr : int = this.prev_hdr;
				var size : uint = this.prev_size;
				var pt : uint;
				var pc : uint;
				var pm : uint;
				var body : ByteArray;
				var p : Object;
				
				while (sck.bytesAvailable > 0)
				{
					if (hdr == -1)
					{
						// A new packet
						hdr = this.sck.readByte();
					}
					pt = (hdr >> 5) & 7;
					pc = (hdr >> 2) & 7;
					pm = (hdr) & 3;
					if (BSPConstants.PACKET_TYPE_RAW == pt || 
						BSPConstants.PACKET_TYPE_OBJ == pt || 
						BSPConstants.PACKET_TYPE_CMD == pt)
					{
						// Read size
						if (size == 0xFFFFFFFF)
						{
							if (BSPConstants.PACKET_MARK_64B == pm)
							{
								if (this.sck.bytesAvailable < 8)
								{
									this.prev_hdr = hdr;
									this.prev_size = 0xFFFFFFFF;
									break;
								}
								
								this.sck.readUnsignedInt();
								size = this.sck.readUnsignedInt();
							}
							
							else
							{
								if (this.sck.bytesAvailable < 4)
								{
									this.prev_hdr = hdr;
									this.prev_size = 0xFFFFFFFF;
									break;
								}
								
								size = this.sck.readInt();
							}
						}
						
						if (sck.bytesAvailable < size)
						{
							// Record
							this.prev_hdr = hdr;
							this.prev_size = size;
							break;
						}
						
						body = new ByteArray();
						body.endian = Endian.BIG_ENDIAN;
						p = new Object();
						this.sck.readBytes(body, 0, size);
						this.prev_hdr = -1;
						this.prev_size = 0xFFFFFFFF;
						
						// A complete body
						if (BSPConstants.COMPRESS_TYPE_DEFLATE == pc)
						{
							body.inflate();
						}
						
						p['type'] = pt;
						p['stream'] = body;
						switch (pt)
						{
							case BSPConstants.PACKET_TYPE_RAW : 
								p['raw'] = body;
								break;
							case BSPConstants.PACKET_TYPE_OBJ : 
								p['obj'] = this.packet.unpack(body, pm);
								break;
							case BSPConstants.PACKET_TYPE_CMD : 
								p['cmd'] = body.readInt();
								p['params'] = this.packet.unpack(body, pm);
								break;
							default : 
								// What the fuck ~~~~
								break;
						}
						
						this.packets.push(p);
						dispatchEvent(new BSPEvent(BSPEvent.BSP_PACKET));
					}
					
					else if (BSPConstants.PACKET_TYPE_REP)
					{
						// Report response
						trace("Response REP");
					}
					
					else
					{
						// Heartbeat
						if (this.hb_timer && this.hb_timer.running)
						{
							var d : Date = new Date();
							var t : int = Math.round(d.getTime() / 1000);
							dispatchEvent(new BSPEvent(BSPEvent.BSP_HEARTBEAT));
							this.last_hb = t;
						}
					}
					
					// Ready for next
					hdr = -1;
					size = 0xFFFFFFFF;
				}
			}
			
			else
			{
				// Stream
				var s : ByteArray = new ByteArray();
				s.endian = Endian.BIG_ENDIAN;
				this.sck.readBytes(s);
				this.streams.push(s);
				dispatchEvent(new BSPEvent(BSPEvent.BSP_STREAM));
			}
			
			return;
		}
		
		private function _hb(e : TimerEvent) : void
		{
			if (this.hb_timer && this.hb_timer.running)
			{
				// Check response first
				var d : Date = new Date();
				var t : int = Math.round(d.getTime() / 1000);
				if (this.last_hb > 0 && t - this.last_hb > this.hb_failure)
				{
					// Heartbeat failure
					dispatchEvent(new BSPEvent(BSPEvent.BSP_SERVER_LOST));
				}
				this.send_hb();
			}
			
			return;
		}
	}
}